@@ -0,0 +1,20 @@ |
||
| 1 |
+$ -> |
|
| 2 |
+ svg = document.querySelector('.agent-diagram svg.diagram')
|
|
| 3 |
+ overlay = document.querySelector('.agent-diagram .overlay')
|
|
| 4 |
+ getTopLeft = (node) -> |
|
| 5 |
+ bbox = node.getBBox() |
|
| 6 |
+ point = svg.createSVGPoint() |
|
| 7 |
+ point.x = bbox.x + bbox.width |
|
| 8 |
+ point.y = bbox.y |
|
| 9 |
+ point.matrixTransform(node.getCTM()) |
|
| 10 |
+ $(svg).find('g.node[data-badge-id]').each ->
|
|
| 11 |
+ tl = getTopLeft(this) |
|
| 12 |
+ $('#' + this.getAttribute('data-badge-id'), overlay).each ->
|
|
| 13 |
+ badge = $(this) |
|
| 14 |
+ badge.css |
|
| 15 |
+ left: tl.x - badge.outerWidth() * (2/3) |
|
| 16 |
+ top: tl.y - badge.outerHeight() * (1/3) |
|
| 17 |
+ 'background-color': badge.find('.label').css('background-color')
|
|
| 18 |
+ .show() |
|
| 19 |
+ return |
|
| 20 |
+ return |
@@ -0,0 +1,30 @@ |
||
| 1 |
+.agent-diagram {
|
|
| 2 |
+ position: relative; |
|
| 3 |
+ z-index: auto; |
|
| 4 |
+ |
|
| 5 |
+ svg.diagram {
|
|
| 6 |
+ position: absolute; |
|
| 7 |
+ z-index: 1; |
|
| 8 |
+ } |
|
| 9 |
+ |
|
| 10 |
+ .overlay-container {
|
|
| 11 |
+ position: absolute; |
|
| 12 |
+ top: 0; |
|
| 13 |
+ left: 0; |
|
| 14 |
+ z-index: auto; |
|
| 15 |
+ |
|
| 16 |
+ .overlay {
|
|
| 17 |
+ position: relative; |
|
| 18 |
+ z-index: auto; |
|
| 19 |
+ width: 100%; |
|
| 20 |
+ height: 100%; |
|
| 21 |
+ |
|
| 22 |
+ .badge {
|
|
| 23 |
+ position: absolute; |
|
| 24 |
+ display: none; |
|
| 25 |
+ color: white !important; |
|
| 26 |
+ z-index: 2; |
|
| 27 |
+ } |
|
| 28 |
+ } |
|
| 29 |
+ } |
|
| 30 |
+} |
@@ -6,7 +6,7 @@ module DotHelper |
||
| 6 | 6 |
dot.close_write |
| 7 | 7 |
dot.read |
| 8 | 8 |
} rescue false) |
| 9 |
- svg.html_safe |
|
| 9 |
+ decorate_svg(svg, agents).html_safe |
|
| 10 | 10 |
else |
| 11 | 11 |
tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri|
|
| 12 | 12 |
uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents)) |
@@ -57,6 +57,13 @@ module DotHelper |
||
| 57 | 57 |
end |
| 58 | 58 |
end |
| 59 | 59 |
|
| 60 |
+ def ids(values) |
|
| 61 |
+ values.each_with_index { |id, i|
|
|
| 62 |
+ raw ' ' if i > 0 |
|
| 63 |
+ id id |
|
| 64 |
+ } |
|
| 65 |
+ end |
|
| 66 |
+ |
|
| 60 | 67 |
def attr_list(attrs = nil) |
| 61 | 68 |
return if attrs.nil? |
| 62 | 69 |
attrs = attrs.select { |key, value| value.present? }
|
@@ -86,16 +93,13 @@ module DotHelper |
||
| 86 | 93 |
end |
| 87 | 94 |
|
| 88 | 95 |
def statement(ids, attrs = nil) |
| 89 |
- Array(ids).each_with_index { |id, i|
|
|
| 90 |
- raw ' ' if i > 0 |
|
| 91 |
- id id |
|
| 92 |
- } |
|
| 96 |
+ ids Array(ids) |
|
| 93 | 97 |
attr_list attrs |
| 94 | 98 |
raw ';' |
| 95 | 99 |
end |
| 96 | 100 |
|
| 97 |
- def block(title, &block) |
|
| 98 |
- raw title |
|
| 101 |
+ def block(*ids, &block) |
|
| 102 |
+ ids ids |
|
| 99 | 103 |
raw '{'
|
| 100 | 104 |
block.call |
| 101 | 105 |
raw '}' |
@@ -112,11 +116,7 @@ module DotHelper |
||
| 112 | 116 |
draw(agents: agents, |
| 113 | 117 |
agent_id: ->agent { 'a%d' % agent.id },
|
| 114 | 118 |
agent_label: ->agent {
|
| 115 |
- if agent.disabled? |
|
| 116 |
- '%s (Disabled)' % agent.name |
|
| 117 |
- else |
|
| 118 |
- agent.name |
|
| 119 |
- end.gsub(/(.{20}\S*)\s+/) {
|
|
| 119 |
+ agent.name.gsub(/(.{20}\S*)\s+/) {
|
|
| 120 | 120 |
# Fold after every 20+ characters |
| 121 | 121 |
$1 + "\n" |
| 122 | 122 |
} |
@@ -128,6 +128,7 @@ module DotHelper |
||
| 128 | 128 |
def agent_node(agent) |
| 129 | 129 |
node(agent_id[agent], |
| 130 | 130 |
label: agent_label[agent], |
| 131 |
+ tooltip: (agent.short_type.titleize if rich), |
|
| 131 | 132 |
URL: (agent_url[agent] if rich), |
| 132 | 133 |
style: ('rounded,dashed' if agent.disabled?),
|
| 133 | 134 |
color: (@disabled if agent.disabled?), |
@@ -141,7 +142,7 @@ module DotHelper |
||
| 141 | 142 |
color: (@disabled if agent.disabled? || receiver.disabled?)) |
| 142 | 143 |
end |
| 143 | 144 |
|
| 144 |
- block('digraph foo') {
|
|
| 145 |
+ block('digraph', 'Agent Event Flow') {
|
|
| 145 | 146 |
# statement 'graph', rankdir: 'LR' |
| 146 | 147 |
statement 'node', |
| 147 | 148 |
shape: 'box', |
@@ -160,4 +161,60 @@ module DotHelper |
||
| 160 | 161 |
} |
| 161 | 162 |
} |
| 162 | 163 |
end |
| 164 |
+ |
|
| 165 |
+ def decorate_svg(xml, agents) |
|
| 166 |
+ svg = Nokogiri::XML(xml).at('svg')
|
|
| 167 |
+ |
|
| 168 |
+ Nokogiri::HTML::Document.new.tap { |doc|
|
|
| 169 |
+ doc << root = Nokogiri::XML::Node.new('div', doc) { |div|
|
|
| 170 |
+ div['class'] = 'agent-diagram' |
|
| 171 |
+ } |
|
| 172 |
+ |
|
| 173 |
+ svg['class'] = 'diagram' |
|
| 174 |
+ |
|
| 175 |
+ root << svg |
|
| 176 |
+ root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div|
|
|
| 177 |
+ div['class'] = 'overlay-container' |
|
| 178 |
+ div['style'] = "width: #{svg['width']}; height: #{svg['height']}"
|
|
| 179 |
+ } |
|
| 180 |
+ overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div|
|
|
| 181 |
+ div['class'] = 'overlay' |
|
| 182 |
+ } |
|
| 183 |
+ |
|
| 184 |
+ svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node|
|
|
| 185 |
+ agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i
|
|
| 186 |
+ agent = agents.find { |a| a.id == agent_id }
|
|
| 187 |
+ |
|
| 188 |
+ count = agent.events_count |
|
| 189 |
+ next unless count && count > 0 |
|
| 190 |
+ |
|
| 191 |
+ overlay << Nokogiri::XML::Node.new('a', doc) { |badge|
|
|
| 192 |
+ badge['id'] = id = 'b%d' % agent_id |
|
| 193 |
+ badge['class'] = 'badge' |
|
| 194 |
+ badge['href'] = events_path(agent: agent) |
|
| 195 |
+ badge['target'] = '_blank' |
|
| 196 |
+ badge['title'] = "#{count} events created"
|
|
| 197 |
+ badge.content = count.to_s |
|
| 198 |
+ |
|
| 199 |
+ node['data-badge-id'] = id |
|
| 200 |
+ |
|
| 201 |
+ badge << Nokogiri::XML::Node.new('span', doc) { |label|
|
|
| 202 |
+ # a dummy label only to obtain the background color |
|
| 203 |
+ label['class'] = [ |
|
| 204 |
+ 'label', |
|
| 205 |
+ if agent.disabled? |
|
| 206 |
+ 'label-warning' |
|
| 207 |
+ elsif agent.working? |
|
| 208 |
+ 'label-success' |
|
| 209 |
+ else |
|
| 210 |
+ 'label-danger' |
|
| 211 |
+ end |
|
| 212 |
+ ].join(' ')
|
|
| 213 |
+ label['style'] = 'display: none'; |
|
| 214 |
+ } |
|
| 215 |
+ } |
|
| 216 |
+ } |
|
| 217 |
+ # See also: app/assets/diagram.js.coffee |
|
| 218 |
+ }.at('div.agent-diagram').to_s
|
|
| 219 |
+ end |
|
| 163 | 220 |
end |
@@ -1,3 +1,7 @@ |
||
| 1 |
+<% content_for :head do %> |
|
| 2 |
+ <%= javascript_include_tag "diagram" %> |
|
| 3 |
+<% end %> |
|
| 4 |
+ |
|
| 1 | 5 |
<div class='container'> |
| 2 | 6 |
<div class='row'> |
| 3 | 7 |
<div class='col-md-12'> |
@@ -61,7 +61,7 @@ Huginn::Application.configure do |
||
| 61 | 61 |
end |
| 62 | 62 |
|
| 63 | 63 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
| 64 |
- config.assets.precompile += %w( graphing.js user_credentials.js ) |
|
| 64 |
+ config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) |
|
| 65 | 65 |
|
| 66 | 66 |
# Ignore bad email addresses and do not raise email delivery errors. |
| 67 | 67 |
# Set this to true and configure the email server for immediate delivery to raise delivery errors. |
@@ -56,13 +56,13 @@ describe DotHelper do |
||
| 56 | 56 |
it "generates a DOT script" do |
| 57 | 57 |
agents_dot(@agents).should =~ %r{
|
| 58 | 58 |
\A |
| 59 |
- digraph \s foo \{
|
|
| 59 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{
|
|
| 60 | 60 |
node \[ [^\]]+ \]; |
| 61 | 61 |
(?<foo>\w+) \[label=foo\]; |
| 62 | 62 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
| 63 | 63 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
| 64 | 64 |
\k<bar1> \[label=bar1\]; |
| 65 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
| 65 |
+ \k<bar2> \[label=bar2,style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
| 66 | 66 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
| 67 | 67 |
\k<bar3> \[label=bar3\]; |
| 68 | 68 |
\} |
@@ -73,15 +73,15 @@ describe DotHelper do |
||
| 73 | 73 |
it "generates a richer DOT script" do |
| 74 | 74 |
agents_dot(@agents, true).should =~ %r{
|
| 75 | 75 |
\A |
| 76 |
- digraph \s foo \{
|
|
| 76 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{
|
|
| 77 | 77 |
node \[ [^\]]+ \]; |
| 78 |
- (?<foo>\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\];
|
|
| 78 |
+ (?<foo>\w+) \[label=foo,tooltip="Dot \x20 Foo",URL="#{Regexp.quote(agent_path(@foo))}"\];
|
|
| 79 | 79 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
| 80 | 80 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
| 81 |
- \k<bar1> \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\];
|
|
| 82 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\];
|
|
| 81 |
+ \k<bar1> \[label=bar1,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar1))}"\];
|
|
| 82 |
+ \k<bar2> \[label=bar2,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\];
|
|
| 83 | 83 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
| 84 |
- \k<bar3> \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\];
|
|
| 84 |
+ \k<bar3> \[label=bar3,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar3))}"\];
|
|
| 85 | 85 |
\} |
| 86 | 86 |
\z |
| 87 | 87 |
}x |